在一開始我們就介紹了 聯合類型和交叉類型 這兩種常用到的高級型別,威爾豬自己也是比較常使用它們。這次我們來看看還有哪些高級型別可以使用:
映射類型是一種 泛型類型,根據一個現有型別的每個屬性都映射為另一個新型別,這在物件的屬性進行轉換或修改時特別有用。
interface IUser {
id: number;
name: string;
greet: () => void;
}
type TMapped<T> = {
[Property in keyof T]: boolean;
};
type TUserMapped = TMapped<IUser>;
const userMapped: TUserMapped = {
id: true,
name: true,
greet: false,
};
在此範例中,TMapped 類型別名從 IUser 接口中取得所有屬性 T,並將其值變更為布林值,然後賦予到新增的 TUserMapped 類型別名上。
在映射期間我們可以使用兩個修飾符:readonly
和 ?
,並透過前綴符號 -
和 +
來刪除或新增這兩個修飾符,如果不加上前綴,則 + 為預設。
interface IReadonlyUser {
readonly id: number;
readonly name: string;
}
// readonly 加入前綴符號 -
type TMapped<T> = {
-readonly [Property in keyof T]: T[Property];
};
type TUnlockUser = TMapped<TReadonlyUser>;
const unlockUser: TUnlockUser = {
id: 1,
name: "威爾豬",
};
unlockUser.name = "威爾羊";
在此範例中,TMapped 類型別名從 IReadonlyUser 接口中取得所有屬性 T,並刪除所有 readonly,然後賦予到新增的 TUnlockUser 類型別名上,這樣類型為 TUnlockUser 的物件屬性值就可以進行修改。
我們再看另一個範例:
interface IUser {
id: string;
name?: string;
age?: number;
}
// 可選屬性加入前綴符號 -
type TMapped<T> = {
[Property in keyof T]-?: T[Property];
};
type TMappedUser = TMapped<IUser>;
在此範例中,TMapped 類型別名從 IUser 接口中取得所有屬性 T,並刪除所有可選屬性,然後賦予到新增的 TMappedUser 類型別名上,這樣類型為 TMappedUser 的物件屬性就必須都存在。
使用方式:
type TMapped<T> = {
[Properties in keyof T as NewKeyType]: T[Properties];
};
看以下範例:
interface IUser {
name: string;
age: number;
location: string;
}
type TMapped<T> = {
[Property in keyof T as `get${Capitalize<
string & Property
>}`]: () => T[Property];
};
type TMappedUser = TMapped<IUser>;
在此範例中,TMapped 類型別名從 IUser 接口中取得所有屬性 T,並使用模板字面值加入 get 和將屬性開頭改為大寫,然後賦予到新增的 TMappedUser 類型別名上,這樣類型為 TMappedUser 的物件屬性就全部重新映射改為方法了。
當然如果我們有只想保留或排除的屬性時 ( 類似之前提到的 Pick 和 Omit ),也可以使用 as 來重新映射類型:
interface IUser {
id: string;
name: string;
age: number;
address: string;
job: string;
}
// 使用 Extract 關鍵字保留屬性
type TMappedExtract<Type> = {
[Property in keyof Type as Extract<
Property,
"id" | "name" | "job"
>]: Type[Property];
};
// 使用 Exclude 關鍵字排除屬性
type TMappedExclude<Type> = {
[Property in keyof Type as Exclude<
Property,
"id" | "address" | "job"
>]: Type[Property];
};
type TMappedExtractUser = TMappedExtract<IUser>;
type TMappedExcludeUser = TMappedExclude<IUser>;
索引類型使用字串或數字索引來獲取和設置物件的屬性值,並根據現有物件的屬性創建新的型別。
看以下範例:
interface IPerson {
[key: string]: string | number; // 使用索引類型
gender: "male" | "female" | "others";
}
type TPersonKeys = keyof IPerson;
const getProperty = (obj: IPerson, key: TPersonKeys): void => {
console.log(obj[key]);
};
const person: IPerson = {
id: 1,
name: "威爾豬",
age: 3,
gender: "male",
};
const personName = getProperty(person, "name"); // 輸出: 威爾豬
const personAddress = getProperty(person, "address"); // 輸出: undefined
在這個範例中,[key: string]: string | number
表示物件可以具有任意字符串索引,用於 getProperty 函數的參數 key 的型別。
條件類型根據條件選擇不同的型別,使用 extends
關鍵字來定義條件。
type TIsString<T> = T extends string ? true : false;
type TStringType = TIsString<string>; // true
type TNumberType = TIsString<number>; // false
這邊不要把 接口 ( interface ) 的 extends 給弄混了,雖然它們在概念上是類似的,但
條件類型中的 extends 用於條件型別的選擇,而 interface 中的 extends 用於接口的繼承
。儘管它們都包含 extends 這個關鍵字,但它們的功能和含義完全不同。
創建 具有模板文字的型別
。
type TColor = "red" | "green" | "blue";
type TCssColor = `bg-${TColor}`;
可變類型是一種 允許函數參數的數量和型別不固定的類型
,也就是可以接受任意數量型別的函數或類型,通常使用在泛型型別中,以便處理具有可變參數數量的情況。
看以下範例:
// 使用 ... 來表示可變數量的泛型參數
type Tuple<T extends unknown[]> = [...T];
const tupleArr1: Tuple<[number, string]> = [1, "哈囉"];
const tupleArr2: Tuple<[number, string, boolean]> = [3, "威爾豬", true];
在這個範例中,Tuple 是一個可變類型,它使用了 [...T]
,表示它接受任意數量的型別參數 T,可以根據提供的型別參數數量組成不同長度的元組型別。
看另一個範例:
const sum = <T extends number[]>(...args: T): number => {
return args.reduce((acc, cur) => acc + cur, 0);
};
console.log(sum(1, 2, 3)); // 輸出: 6
console.log(sum(1, 2, 3, 4, 5)); // 輸出: 15
在這個範例中,sum 函式參數使用了可變類型,表示它接受任意數量的數字參數,並返回它們的總和。
字面量類型可以 確保變數只能被賦值為特定的字面值
,而不僅僅是一個範圍,這對於指定特定的字串、數字等非常有用,從而增強型別的安全性。
type TDirection = "up" | "down" | "left" | "right";
const userMove = (direction: TDirection): void => {
console.log(`使用者向 ${direction} 移動`);
};
userMove("left"); // 輸出: 使用者向 left 移動
當然我們可以使用高級型別來做結合,以實現更靈活的型別操作。通常映射類型和索引類型會一起使用,以動態生成新型別。
interface IUser {
id: number;
name: string;
}
// 使用映射類型和索引類型
type TUpdateUser<T, U extends keyof T> = {
[Property in U]?: T[Property];
};
const updateUser = <T, U extends keyof T>(
user: T,
updateObj: TUpdateUser<T, U>
): T => {
return { ...user, ...updateObj };
};
const user: IUser = { id: 1, name: "威爾豬" };
const updatedUser = updateUser(user, { name: "威爾羊" });
console.log(updatedUser); // 輸出: {id: 1, name: '威爾羊'}
這段程式碼中,我們使用了映射類型和索引類型的組合,創建了一個類別別名 TUpdateUser。這個類別別名使用 keyof T 來獲取型別 T 中所有的屬性,並將其映射到一個新的類型別名 TUpdateUser 上。TUpdateUser 型別可以使用 U 中的每個屬性,並將這些屬性的值設定為 T 中對應屬性的可選項目。
高級型別能夠幫助我們更靈活地處理複雜的型別需求,使程式碼更具彈性和維護性,端看我們如何結合和運用。通過運用這些高級型別,我們可以在開發過程中更精確地定義型別,能夠更好地捕捉程式碼中的錯誤。